// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package io.nodyn.dns;

import java.io.*;
import java.lang.reflect.Method;
import java.util.*;

/**
 * A class that tries to locate name servers and the search path to
 * be appended to unqualified names.
 * <p/>
 * The following are attempted, in order, until one succeeds.
 * <UL>
 * <LI>The properties 'dns.server' and 'dns.search' (comma delimited lists)
 * are checked.  The servers can either be IP addresses or hostnames
 * (which are resolved using Java's built in DNS support).
 * <LI>The sun.net.dns.ResolverConfiguration class is queried.
 * <LI>On Unix, /etc/resolv.conf is parsed.
 * <LI>On Windows, ipconfig/winipcfg is called and its output parsed.  This
 * may fail for non-English versions on Windows.
 * <LI>"localhost" is used as the nameserver, and the search path is empty.
 * </UL>
 * <p/>
 * These routines will be called internally when creating Resolvers/Lookups
 * without explicitly specifying server names, and can also be called
 * directly if desired.
 *
 * @author Brian Wellington
 * @author <a href="mailto:yannick@meudal.net">Yannick Meudal</a>
 * @author <a href="mailto:arnt@gulbrandsen.priv.no">Arnt Gulbrandsen</a>
 */

public class ResolverConfig {

    private String[] servers = null;
    private int ndots = -1;

    private static ResolverConfig currentConfig;

    static {
        refresh();
    }

    public ResolverConfig() {
        if (findProperty()) {
            return;
        }
        if (findSunJVM()) {
            return;
        }
        if (this.servers == null) {
            String OS = System.getProperty("os.name");
            String vendor = System.getProperty("java.vendor");
            if (OS.indexOf("Windows") != -1) {
                if (OS.indexOf("95") != -1 ||
                        OS.indexOf("98") != -1 ||
                        OS.indexOf("ME") != -1)
                    find95();
                else
                    findNT();
            } else if (OS.indexOf("NetWare") != -1) {
                findNetware();
            } else if (vendor.indexOf("Android") != -1) {
                findAndroid();
            } else {
                findUnix();
            }
        }
    }

    private void
    addServer(String server, List list) {
        if (list.contains(server)) {
            return;
        }
        list.add(server);
    }

    private int
    parseNdots(String token) {
        token = token.substring(6);
        try {
            int ndots = Integer.parseInt(token);
            if (ndots >= 0) {
                return ndots;
            }
        } catch (NumberFormatException e) {
        }
        return -1;
    }

    private void
    configureFromLists(List lserver) {
        if (servers == null && lserver.size() > 0) {
            servers = (String[]) lserver.toArray(new String[0]);
        }
    }

    private void
    configureNdots(int lndots) {
        if (ndots < 0 && lndots > 0)
            ndots = lndots;
    }

    /**
     * Looks in the system properties to find servers and a search path.
     * Servers are defined by dns.server=server1,server2...
     * The search path is defined by dns.search=domain1,domain2...
     */
    private boolean findProperty() {
        String prop;
        List lserver = new ArrayList(0);
        StringTokenizer st;

        prop = System.getProperty("dns.server");
        if (prop != null) {
            st = new StringTokenizer(prop, ",");
            while (st.hasMoreTokens())
                addServer(st.nextToken(), lserver);
        }
        configureFromLists(lserver);
        return (servers != null);
    }

    /**
     * Uses the undocumented Sun DNS implementation to determine the configuration.
     * This doesn't work or even compile with all JVMs (gcj, for example).
     */
    private boolean
    findSunJVM() {
        List lserver = new ArrayList(0);
        List lserver_tmp;

        try {
            Class[] noClasses = new Class[0];
            Object[] noObjects = new Object[0];
            String resConfName = "sun.net.dns.ResolverConfiguration";
            Class resConfClass = Class.forName(resConfName);
            Object resConf;

            // ResolverConfiguration resConf = ResolverConfiguration.open();
            Method open = resConfClass.getDeclaredMethod("open", noClasses);
            resConf = open.invoke(null, noObjects);

            // lserver_tmp = resConf.nameservers();
            Method nameservers = resConfClass.getMethod("nameservers",
                    noClasses);
            lserver_tmp = (List) nameservers.invoke(resConf, noObjects);
        } catch (Exception e) {
            return false;
        }

        if (lserver_tmp.size() == 0)
            return false;

        if (lserver_tmp.size() > 0) {
            Iterator it = lserver_tmp.iterator();
            while (it.hasNext())
                addServer((String) it.next(), lserver);
        }

        configureFromLists(lserver);
        return true;
    }

    /**
     * Looks in /etc/resolv.conf to find servers and a search path.
     * "nameserver" lines specify servers.  "domain" and "search" lines
     * define the search path.
     */
    private void
    findResolvConf(String file) {
        InputStream in = null;
        try {
            in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            return;
        }
        InputStreamReader isr = new InputStreamReader(in);
        BufferedReader br = new BufferedReader(isr);
        List lserver = new ArrayList(0);

        int lndots = -1;
        try {
            String line;
            while ((line = br.readLine()) != null) {
                if (line.startsWith("nameserver")) {
                    StringTokenizer st = new StringTokenizer(line);
                    st.nextToken(); /* skip nameserver */
                    addServer(st.nextToken(), lserver);
                } else if (line.startsWith("domain")) {
                    continue;
                } else if (line.startsWith("search")) {
                    continue;
                } else if (line.startsWith("options")) {
                    StringTokenizer st = new StringTokenizer(line);
                    st.nextToken(); /* skip options */
                    while (st.hasMoreTokens()) {
                        String token = st.nextToken();
                        if (token.startsWith("ndots:")) {
                            lndots = parseNdots(token);
                        }
                    }
                }
            }
            br.close();
        } catch (IOException e) {
        }

        configureFromLists(lserver);
        configureNdots(lndots);
    }

    private void
    findUnix() {
        findResolvConf("/etc/resolv.conf");
    }

    private void
    findNetware() {
        findResolvConf("sys:/etc/resolv.cfg");
    }

    /**
     * Parses the output of winipcfg or ipconfig.
     */
    private void
    findWin(InputStream in, Locale locale) {
        String packageName = ResolverConfig.class.getPackage().getName();
        String resPackageName = packageName + ".windows.DNSServer";
        ResourceBundle res;
        if (locale != null)
            res = ResourceBundle.getBundle(resPackageName, locale);
        else
            res = ResourceBundle.getBundle(resPackageName);

        String host_name = res.getString("host_name");
        String primary_dns_suffix = res.getString("primary_dns_suffix");
        String dns_suffix = res.getString("dns_suffix");
        String dns_servers = res.getString("dns_servers");

        BufferedReader br = new BufferedReader(new InputStreamReader(in));
        try {
            List lserver = new ArrayList();
            String line = null;
            boolean readingServers = false;
            boolean readingSearches = false;
            while ((line = br.readLine()) != null) {
                StringTokenizer st = new StringTokenizer(line);
                if (!st.hasMoreTokens()) {
                    readingServers = false;
                    readingSearches = false;
                    continue;
                }
                String s = st.nextToken();
                if (line.indexOf(":") != -1) {
                    readingServers = false;
                    readingSearches = false;
                }

                if (line.indexOf(host_name) != -1) {
                    while (st.hasMoreTokens()) {
                        s = st.nextToken();
                    }
                } else if (line.indexOf(primary_dns_suffix) != -1) {
                    while (st.hasMoreTokens()) {
                        s = st.nextToken();
                    }
                    readingSearches = true;
                } else if (readingSearches || line.indexOf(dns_suffix) != -1) {
                    while (st.hasMoreTokens()) {
                        s = st.nextToken();
                    }
                    if (s.equals(":")) {
                        continue;
                    }
                    readingSearches = true;
                } else if (readingServers || line.indexOf(dns_servers) != -1) {
                    while (st.hasMoreTokens()) {
                        s = st.nextToken();
                    }
                    if (s.equals(":")) {
                        continue;
                    }
                    readingServers = true;
                }
            }

            configureFromLists(lserver);
        } catch (IOException e) {
        }
        return;
    }

    private void
    findWin(InputStream in) {
        String property = "org.xbill.DNS.windows.parse.buffer";
        final int defaultBufSize = 8 * 1024;
        int bufSize = Integer.getInteger(property, defaultBufSize).intValue();
        BufferedInputStream b = new BufferedInputStream(in, bufSize);
        b.mark(bufSize);
        findWin(b, null);
        if (servers == null) {
            try {
                b.reset();
            } catch (IOException e) {
                return;
            }
            findWin(b, new Locale("", ""));
        }
    }

    /**
     * Calls winipcfg and parses the result to find servers and a search path.
     */
    private void
    find95() {
        String s = "winipcfg.out";
        try {
            Process p;
            p = Runtime.getRuntime().exec("winipcfg /all /batch " + s);
            p.waitFor();
            File f = new File(s);
            findWin(new FileInputStream(f));
            new File(s).delete();
        } catch (Exception e) {
            return;
        }
    }

    /**
     * Calls ipconfig and parses the result to find servers and a search path.
     */
    private void
    findNT() {
        try {
            Process p;
            p = Runtime.getRuntime().exec("ipconfig /all");
            findWin(p.getInputStream());
            p.destroy();
        } catch (Exception e) {
            return;
        }
    }

    /**
     * Parses the output of getprop, which is the only way to get DNS
     * info on Android. getprop might disappear in future releases, so
     * this code comes with a use-by date.
     */
    private void
    findAndroid() {
        // This originally looked for all lines containing .dns; but
        // http://code.google.com/p/android/issues/detail?id=2207#c73
        // indicates that net.dns* should always be the active nameservers, so
        // we use those.
        final String re1 = "^\\d+(\\.\\d+){3}$";
        final String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
        ArrayList lserver = new ArrayList();
        try {
            Class SystemProperties =
                    Class.forName("android.os.SystemProperties");
            Method method =
                    SystemProperties.getMethod("get",
                            new Class[]{String.class});
            final String[] netdns = new String[]{"net.dns1", "net.dns2",
                    "net.dns3", "net.dns4"};
            for (int i = 0; i < netdns.length; i++) {
                Object[] args = new Object[]{netdns[i]};
                String v = (String) method.invoke(null, args);
                if (v != null &&
                        (v.matches(re1) || v.matches(re2)) &&
                        !lserver.contains(v))
                    lserver.add(v);
            }
        } catch (Exception e) {
            // ignore resolutely
        }
        configureFromLists(lserver);
    }

    /**
     * Returns all located servers
     */
    public String[] servers() {
        return servers;
    }

    /**
     * Returns the first located server
     */
    public String
    server() {
        if (servers == null)
            return null;
        return servers[0];
    }

    /**
     * Returns the located ndots value, or the default (1) if not configured.
     * Note that ndots can only be configured in a resolv.conf file, and will only
     * take effect if ResolverConfig uses resolv.conf directly (that is, if the
     * JVM does not include the sun.net.dns.ResolverConfiguration class).
     */
    public int ndots() {
        if (ndots < 0) {
            return 1;
        }
        return ndots;
    }

    /**
     * Gets the current configuration
     */
    public static synchronized ResolverConfig getCurrentConfig() {
        return currentConfig;
    }

    /**
     * Gets the current configuration
     */
    public static void refresh() {
        ResolverConfig newConfig = new ResolverConfig();
        synchronized (ResolverConfig.class) {
            currentConfig = newConfig;
        }
    }

}